﻿using System.Globalization;
using Nop.Core;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Directory;
using Nop.Core.Domain.Tax;
using Nop.Services.Directory;
using Nop.Services.Localization;

namespace Nop.Services.Catalog;

/// <summary>
/// Price formatter
/// </summary>
public partial class PriceFormatter : IPriceFormatter
{
    #region Fields

    protected readonly CurrencySettings _currencySettings;
    protected readonly ICurrencyService _currencyService;
    protected readonly ILocalizationService _localizationService;
    protected readonly IMeasureService _measureService;
    protected readonly IPriceCalculationService _priceCalculationService;
    protected readonly IWorkContext _workContext;
    protected readonly TaxSettings _taxSettings;

    #endregion

    #region Ctor

    public PriceFormatter(CurrencySettings currencySettings,
        ICurrencyService currencyService,
        ILocalizationService localizationService,
        IMeasureService measureService,
        IPriceCalculationService priceCalculationService,
        IWorkContext workContext,
        TaxSettings taxSettings)
    {
        _currencySettings = currencySettings;
        _currencyService = currencyService;
        _localizationService = localizationService;
        _measureService = measureService;
        _priceCalculationService = priceCalculationService;
        _workContext = workContext;
        _taxSettings = taxSettings;
    }

    #endregion

    #region Utilities

    /// <summary>
    /// Gets currency string
    /// </summary>
    /// <param name="amount">Amount</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="targetCurrency">Target currency</param>
    /// <returns>Currency string without exchange rate</returns>
    protected virtual string GetCurrencyString(decimal amount,
        bool showCurrency, Currency targetCurrency)
    {
        ArgumentNullException.ThrowIfNull(targetCurrency);

        string result;
        if (!string.IsNullOrEmpty(targetCurrency.CustomFormatting))
            //custom formatting specified by a store owner
            result = amount.ToString(targetCurrency.CustomFormatting);
        else
        {
            if (!string.IsNullOrEmpty(targetCurrency.DisplayLocale))
                //default behavior
                result = amount.ToString("C", new CultureInfo(targetCurrency.DisplayLocale));
            else
            {
                //not possible because "DisplayLocale" should be always specified
                //but anyway let's just handle this behavior
                result = $"{amount:N} ({targetCurrency.CurrencyCode})";
                return result;
            }
        }

        //display currency code?
        if (showCurrency && _currencySettings.DisplayCurrencyLabel)
            result = $"{result} ({targetCurrency.CurrencyCode})";
        return result;
    }

    /// <summary>
    /// Formats the shipping price
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="targetCurrency">Target currency</param>
    /// <param name="languageId">Language</param>
    /// <param name="priceIncludesTax">A value indicating whether price includes tax</param>
    /// <param name="showTax">A value indicating whether to show tax suffix</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    protected virtual async Task<string> FormatShippingPriceAsync(decimal price, bool showCurrency,
        Currency targetCurrency, int languageId, bool priceIncludesTax, bool showTax)
    {
        return await FormatPriceAsync(price, showCurrency, targetCurrency, languageId, priceIncludesTax, showTax);
    }

    /// <summary>
    /// Formats the payment method additional fee
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="targetCurrency">Target currency</param>
    /// <param name="languageId">Language</param>
    /// <param name="priceIncludesTax">A value indicating whether price includes tax</param>
    /// <param name="showTax">A value indicating whether to show tax suffix</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    protected virtual async Task<string> FormatPaymentMethodAdditionalFeeAsync(decimal price, bool showCurrency,
        Currency targetCurrency, int languageId, bool priceIncludesTax, bool showTax)
    {
        return await FormatPriceAsync(price, showCurrency, targetCurrency, languageId,
            priceIncludesTax, showTax);
    }

    #endregion

    #region Methods

    /// <summary>
    /// Formats the price
    /// </summary>
    /// <param name="price">Price</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatPriceAsync(decimal price)
    {
        return await FormatPriceAsync(price, true, await _workContext.GetWorkingCurrencyAsync());
    }

    /// <summary>
    /// Formats the price
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="targetCurrency">Target currency</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatPriceAsync(decimal price, bool showCurrency, Currency targetCurrency)
    {
        var priceIncludesTax = await _workContext.GetTaxDisplayTypeAsync() == TaxDisplayType.IncludingTax;
        return await FormatPriceAsync(price, showCurrency, targetCurrency, (await _workContext.GetWorkingLanguageAsync()).Id, priceIncludesTax);
    }

    /// <summary>
    /// Formats the price
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="showTax">A value indicating whether to show tax suffix</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatPriceAsync(decimal price, bool showCurrency, bool showTax)
    {
        var priceIncludesTax = await _workContext.GetTaxDisplayTypeAsync() == TaxDisplayType.IncludingTax;
        return await FormatPriceAsync(price, showCurrency, await _workContext.GetWorkingCurrencyAsync(), (await _workContext.GetWorkingLanguageAsync()).Id, priceIncludesTax, showTax);
    }

    /// <summary>
    /// Formats the price
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="currencyCode">Currency code</param>
    /// <param name="showTax">A value indicating whether to show tax suffix</param>
    /// <param name="languageId">Language</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatPriceAsync(decimal price, bool showCurrency,
        string currencyCode, bool showTax, int languageId)
    {
        var currency = await _currencyService.GetCurrencyByCodeAsync(currencyCode) ?? new Currency
        {
            CurrencyCode = currencyCode
        };

        var priceIncludesTax = await _workContext.GetTaxDisplayTypeAsync() == TaxDisplayType.IncludingTax;
        return await FormatPriceAsync(price, showCurrency, currency, languageId, priceIncludesTax, showTax);
    }

    /// <summary>
    /// Formats the order price
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="currencyRate">Currency rate</param>
    /// <param name="customerCurrencyCode">Customer currency code</param>
    /// <param name="displayCustomerCurrency">A value indicating whether to display price on customer currency</param>
    /// <param name="primaryStoreCurrency">Primary store currency</param>
    /// <param name="languageId">Language</param>
    /// <param name="priceIncludesTax">A value indicating whether price includes tax</param>
    /// <param name="showTax">A value indicating whether to show tax suffix</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatOrderPriceAsync(decimal price,
        decimal currencyRate, string customerCurrencyCode, bool displayCustomerCurrency,
        Currency primaryStoreCurrency, int languageId, bool? priceIncludesTax = null, bool? showTax = null)
    {
        var needAddPriceOnCustomerCurrency = primaryStoreCurrency.CurrencyCode != customerCurrencyCode && displayCustomerCurrency;
        var includesTax = priceIncludesTax ?? await _workContext.GetTaxDisplayTypeAsync() == TaxDisplayType.IncludingTax;
        var priceText = await FormatPriceAsync(price, true, primaryStoreCurrency,
            languageId, includesTax, showTax ?? _taxSettings.DisplayTaxSuffix);

        if (!needAddPriceOnCustomerCurrency || await _currencyService.GetCurrencyByCodeAsync(customerCurrencyCode) is not Currency currency)
            return priceText;

        var customerPrice = _currencyService.ConvertCurrency(price, currencyRate);
        var customerPriceText = await FormatPriceAsync(customerPrice, true, currency,
            languageId, includesTax, showTax ?? _taxSettings.DisplayTaxSuffix);
        priceText += $"<br />[{customerPriceText}]";

        return priceText;
    }

    /// <summary>
    /// Formats the price
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="currencyCode">Currency code</param>
    /// <param name="languageId">Language</param>
    /// <param name="priceIncludesTax">A value indicating whether price includes tax</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatPriceAsync(decimal price, bool showCurrency,
        string currencyCode, int languageId, bool priceIncludesTax)
    {
        var currency = await _currencyService.GetCurrencyByCodeAsync(currencyCode)
                       ?? new Currency
                       {
                           CurrencyCode = currencyCode
                       };
        return await FormatPriceAsync(price, showCurrency, currency, languageId, priceIncludesTax);
    }

    /// <summary>
    /// Formats the price
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="targetCurrency">Target currency</param>
    /// <param name="languageId">Language</param>
    /// <param name="priceIncludesTax">A value indicating whether price includes tax</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatPriceAsync(decimal price, bool showCurrency,
        Currency targetCurrency, int languageId, bool priceIncludesTax)
    {
        return await FormatPriceAsync(price, showCurrency, targetCurrency, languageId,
            priceIncludesTax, _taxSettings.DisplayTaxSuffix);
    }

    /// <summary>
    /// Formats the price
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="targetCurrency">Target currency</param>
    /// <param name="languageId">Language</param>
    /// <param name="priceIncludesTax">A value indicating whether price includes tax</param>
    /// <param name="showTax">A value indicating whether to show tax suffix</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatPriceAsync(decimal price, bool showCurrency,
        Currency targetCurrency, int languageId, bool priceIncludesTax, bool showTax)
    {
        //we should round it no matter of "ShoppingCartSettings.RoundPricesDuringCalculation" setting
        price = await _priceCalculationService.RoundPriceAsync(price, targetCurrency);

        var currencyString = GetCurrencyString(price, showCurrency, targetCurrency);
        if (!showTax)
            return currencyString;

        //show tax suffix
        string formatStr;
        if (priceIncludesTax)
        {
            formatStr = await _localizationService.GetResourceAsync("Products.InclTaxSuffix", languageId, false);
            if (string.IsNullOrEmpty(formatStr))
                formatStr = "{0} incl tax";
        }
        else
        {
            formatStr = await _localizationService.GetResourceAsync("Products.ExclTaxSuffix", languageId, false);
            if (string.IsNullOrEmpty(formatStr))
                formatStr = "{0} excl tax";
        }

        return string.Format(formatStr, currencyString);
    }

    /// <summary>
    /// Formats the price of rental product (with rental period)
    /// </summary>
    /// <param name="product">Product</param>
    /// <param name="price">Price</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the rental product price with period
    /// </returns>
    public virtual async Task<string> FormatRentalProductPeriodAsync(Product product, string price)
    {
        ArgumentNullException.ThrowIfNull(product);

        if (!product.IsRental)
            return price;

        if (string.IsNullOrWhiteSpace(price))
            return price;
        var result = product.RentalPricePeriod switch
        {
            RentalPricePeriod.Days => string.Format(await _localizationService.GetResourceAsync("Products.Price.Rental.Days"), price, product.RentalPriceLength),
            RentalPricePeriod.Weeks => string.Format(await _localizationService.GetResourceAsync("Products.Price.Rental.Weeks"), price, product.RentalPriceLength),
            RentalPricePeriod.Months => string.Format(await _localizationService.GetResourceAsync("Products.Price.Rental.Months"), price, product.RentalPriceLength),
            RentalPricePeriod.Years => string.Format(await _localizationService.GetResourceAsync("Products.Price.Rental.Years"), price, product.RentalPriceLength),
            _ => throw new NopException("Not supported rental period"),
        };
        return result;
    }

    /// <summary>
    /// Formats the shipping price
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatShippingPriceAsync(decimal price, bool showCurrency)
    {
        var priceIncludesTax = await _workContext.GetTaxDisplayTypeAsync() == TaxDisplayType.IncludingTax;
        return await FormatShippingPriceAsync(price, showCurrency, await _workContext.GetWorkingCurrencyAsync(), (await _workContext.GetWorkingLanguageAsync()).Id, priceIncludesTax);
    }

    /// <summary>
    /// Formats the shipping price
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="targetCurrency">Target currency</param>
    /// <param name="languageId">Language</param>
    /// <param name="priceIncludesTax">A value indicating whether price includes tax</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatShippingPriceAsync(decimal price, bool showCurrency,
        Currency targetCurrency, int languageId, bool priceIncludesTax)
    {
        var showTax = _taxSettings.ShippingIsTaxable && _taxSettings.DisplayTaxSuffix;
        return await FormatShippingPriceAsync(price, showCurrency, targetCurrency, languageId, priceIncludesTax, showTax);
    }

    /// <summary>
    /// Formats the shipping price
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="currencyCode">Currency code</param>
    /// <param name="languageId">Language</param>
    /// <param name="priceIncludesTax">A value indicating whether price includes tax</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatShippingPriceAsync(decimal price, bool showCurrency,
        string currencyCode, int languageId, bool priceIncludesTax)
    {
        var currency = await _currencyService.GetCurrencyByCodeAsync(currencyCode)
                       ?? new Currency
                       {
                           CurrencyCode = currencyCode
                       };
        return await FormatShippingPriceAsync(price, showCurrency, currency, languageId, priceIncludesTax);
    }

    /// <summary>
    /// Formats the payment method additional fee
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatPaymentMethodAdditionalFeeAsync(decimal price, bool showCurrency)
    {
        var priceIncludesTax = await _workContext.GetTaxDisplayTypeAsync() == TaxDisplayType.IncludingTax;

        return await FormatPaymentMethodAdditionalFeeAsync(price, showCurrency, await _workContext.GetWorkingCurrencyAsync(),
            (await _workContext.GetWorkingLanguageAsync()).Id, priceIncludesTax);
    }

    /// <summary>
    /// Formats the payment method additional fee
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="targetCurrency">Target currency</param>
    /// <param name="languageId">Language</param>
    /// <param name="priceIncludesTax">A value indicating whether price includes tax</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatPaymentMethodAdditionalFeeAsync(decimal price, bool showCurrency,
        Currency targetCurrency, int languageId, bool priceIncludesTax)
    {
        var showTax = _taxSettings.PaymentMethodAdditionalFeeIsTaxable && _taxSettings.DisplayTaxSuffix;
        return await FormatPaymentMethodAdditionalFeeAsync(price, showCurrency, targetCurrency, languageId, priceIncludesTax, showTax);
    }

    /// <summary>
    /// Formats the payment method additional fee
    /// </summary>
    /// <param name="price">Price</param>
    /// <param name="showCurrency">A value indicating whether to show a currency</param>
    /// <param name="currencyCode">Currency code</param>
    /// <param name="languageId">Language</param>
    /// <param name="priceIncludesTax">A value indicating whether price includes tax</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the price
    /// </returns>
    public virtual async Task<string> FormatPaymentMethodAdditionalFeeAsync(decimal price, bool showCurrency,
        string currencyCode, int languageId, bool priceIncludesTax)
    {
        var currency = await _currencyService.GetCurrencyByCodeAsync(currencyCode)
                       ?? new Currency
                       {
                           CurrencyCode = currencyCode
                       };
        return await FormatPaymentMethodAdditionalFeeAsync(price, showCurrency, currency,
            languageId, priceIncludesTax);
    }

    /// <summary>
    /// Formats a tax rate
    /// </summary>
    /// <param name="taxRate">Tax rate</param>
    /// <returns>Formatted tax rate</returns>
    public virtual string FormatTaxRate(decimal taxRate)
    {
        return taxRate.ToString("G29", CultureInfo.InvariantCulture);
    }

    /// <summary>
    /// Format base price (PAngV)
    /// </summary>
    /// <param name="product">Product</param>
    /// <param name="productPrice">Product price (in primary currency). Pass null if you want to use a default produce price</param>
    /// <param name="totalWeight">Total weight of product (with attribute weight adjustment). Pass null if you want to use a default produce weight</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the base price
    /// </returns>
    public virtual async Task<string> FormatBasePriceAsync(Product product, decimal? productPrice, decimal? totalWeight = null)
    {
        ArgumentNullException.ThrowIfNull(product);

        if (!product.BasepriceEnabled)
            return null;

        var productAmount = totalWeight.HasValue && totalWeight.Value > decimal.Zero ? totalWeight.Value : product.BasepriceAmount;
        //Amount in product cannot be 0
        if (productAmount == 0)
            return null;
        var referenceAmount = product.BasepriceBaseAmount;
        var productUnit = await _measureService.GetMeasureWeightByIdAsync(product.BasepriceUnitId);
        //measure weight cannot be loaded
        if (productUnit == null)
            return null;
        var referenceUnit = await _measureService.GetMeasureWeightByIdAsync(product.BasepriceBaseUnitId);
        //measure weight cannot be loaded
        if (referenceUnit == null)
            return null;

        productPrice ??= product.Price;

        var basePrice = productPrice.Value /
                        //do not round. otherwise, it can cause issues
                        await _measureService.ConvertWeightAsync(productAmount, productUnit, referenceUnit, false) *
                        referenceAmount;
        var basePriceInCurrentCurrency = await _currencyService.ConvertFromPrimaryStoreCurrencyAsync(basePrice, await _workContext.GetWorkingCurrencyAsync());
        var basePriceStr = await FormatPriceAsync(basePriceInCurrentCurrency, true, false);

        var result = string.Format(await _localizationService.GetResourceAsync("Products.BasePrice"),
            basePriceStr, referenceAmount.ToString("G29"), referenceUnit.Name);
        return result;
    }

    #endregion
}